package com.puzzlebazar.client.puzzle.view;


import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.MouseDownEvent;
import com.google.gwt.event.dom.client.MouseDownHandler;
import com.google.gwt.event.dom.client.MouseMoveEvent;
import com.google.gwt.event.dom.client.MouseMoveHandler;
import com.google.gwt.event.dom.client.MouseUpEvent;
import com.google.gwt.event.dom.client.MouseUpHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.ui.AbsolutePanel;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.puzzlebazar.client.main.view.ScreenSizeListener;
import com.puzzlebazar.client.puzzle.model.History;
import com.puzzlebazar.client.puzzle.model.HistoryMove;
import com.puzzlebazar.client.puzzle.model.Move;
import com.puzzlebazar.client.puzzle.model.MoveTimer;
import com.puzzlebazar.client.puzzle.model.PuzzleModel;
import com.puzzlebazar.client.util.AbsolutePanelWithControl;
import com.puzzlebazar.client.util.Observable;
import com.puzzlebazar.client.util.ObservableImpl;
import com.puzzlebazar.client.util.Observer;
import com.puzzlebazar.client.util.Vec2i;

/**
 * @author beaudoin
 *
 */
public class HistoryView extends Composite implements Observer, ScreenSizeListener {

	protected static final int BAR_CONTAINER_HEIGHT = 40;
	protected static final int BAR_HEIGHT = 8;	
	protected static final int BAR_MARGIN_X = 12;

	private static final int BUTTON_BAR_WIDTH = 100;
	private static final int BUTTON_BAR_HEIGHT = 18;
	
	private static final int PLAYBACK_SPEED_BAR_NB_BUTTONS = 8;
	private static final int PLAYBACK_SPEED_BAR_WIDTH = 100;
	private static final int PLAYBACK_SPEED_BAR_HEIGHT = 12;
	
	private static final int CONTROL_PANEL_WIDTH = 100;
	private static final int CONTROL_PANEL_HEIGHT = 30;
	private static final int CONTROL_PANEL_Y = 5;	
	
	private static final int TIME_MARKER_HEIGHT = 22;
	private static final int TIME_MARKER_WIDTH = 2;
	
	private static final String firstUrl = "timeline/rew.png"; 
	private static final String prevUrl = "timeline/prev.png"; 
	private static final String playUrl = "timeline/play.png"; 
	private static final String pauseUrl = "timeline/pause.png";
	private static final String nextUrl = "timeline/next.png";
	private static final String lastUrl = "timeline/ffwd.png";
	
	protected final History history;
	private final MoveTimer moveTimer;
	protected final PuzzleModel puzzleModel;
	
	protected final AbsolutePanel container = new AbsolutePanel();

	protected final AbsolutePanelWithControl barContainer = new AbsolutePanelWithControl();
	private final AbsolutePanel bar = new AbsolutePanel();
	private final AbsolutePanel timeMarker = new AbsolutePanel();
	private final VerticalPanel controlPanel = new VerticalPanel();
	private final HorizontalPanel buttonBar = new HorizontalPanel();
	private final PlaybackSpeedBar playbackSpeedBar;
	
	protected int barWidth;
	
	private final Image buttonPlay;
	private final Image buttonPause;
	
	protected final HistoryViewInfo historyViewInfo = new HistoryViewInfo();
	
	private HandlerRegistration mouseMoveHandler;

	public HistoryView( final History history, final MoveTimer moveTimer, final PuzzleModel puzzleModel ) {
		barWidth = 1;
		
		container.setWidth( "100%" );
		container.setHeight( getTotalHeight() + "px" );
		
		barContainer.setWidth( "100%" );
		barContainer.setHeight( BAR_CONTAINER_HEIGHT + "px" );
		mouseMoveHandler = null;
		
		barContainer.addMouseDownHandler( new MouseDownHandler(){
			@Override
			public void onMouseDown(MouseDownEvent event) {
				if( event.getNativeButton() == NativeEvent.BUTTON_LEFT ) {
					setTime( event.getRelativeX(bar.getElement()) );
					if( mouseMoveHandler == null ) {
						DOM.setCapture( barContainer.getElement() );
						mouseMoveHandler = barContainer.addMouseMoveHandler(new MouseMoveHandler(){
							@Override
							public void onMouseMove(MouseMoveEvent event) {							
								setTime( event.getRelativeX(bar.getElement()) );
							}
						});
					}
					event.stopPropagation();
				}
			}} );

		barContainer.addMouseUpHandler( new MouseUpHandler() {
			@Override
			public void onMouseUp(MouseUpEvent event) {
				if( event.getNativeButton() != NativeEvent.BUTTON_LEFT )
					return;
				if( mouseMoveHandler != null ) {
					DOM.releaseCapture( barContainer.getElement() );
					mouseMoveHandler.removeHandler();
					mouseMoveHandler = null;		
				}
				event.stopPropagation();
			}
		});
		
		bar.setStylePrimaryName("grayfill");
		
		timeMarker.setStylePrimaryName("blackfill");
		timeMarker.setPixelSize( TIME_MARKER_WIDTH, TIME_MARKER_HEIGHT );
		
		Image buttonFirst = new Image(firstUrl);
		buttonFirst.addClickHandler( new ClickHandler(){
			@Override
			public void onClick(ClickEvent event) {
				try {
					HistoryMove move = history.undoAll(puzzleModel);
					puzzleModel.notifyObservers( move.getPuzzleMove() );
					history.notifyObservers( move );
				} catch (Exception e) {
					e.printStackTrace();
				}
			}} );

		buttonFirst.setStylePrimaryName("selectable");

		Image buttonPrev = new Image(prevUrl);
		buttonPrev.addClickHandler( new ClickHandler(){
			@Override
			public void onClick(ClickEvent event) {
				try {
					HistoryMove move = history.undo(puzzleModel);
					puzzleModel.notifyObservers( move.getPuzzleMove() );
					history.notifyObservers( move );
				} catch (Exception e) {
					e.printStackTrace();
				}
			}} );
		buttonPrev.setStylePrimaryName("selectable");

		buttonPlay = new Image(playUrl);
		buttonPlay.addClickHandler( new ClickHandler(){
			@Override
			public void onClick(ClickEvent event) {
					moveTimer.start();
					moveTimer.notifyObservers();
			}} );
		buttonPlay.setStylePrimaryName("selectable");

		buttonPause = new Image(pauseUrl);
		buttonPause.addClickHandler( new ClickHandler(){
			@Override
			public void onClick(ClickEvent event) {
					moveTimer.stop();
					moveTimer.notifyObservers();
			}} );
		buttonPause.setStylePrimaryName("selectable");

		Image buttonNext = new Image(nextUrl);
		buttonNext.addClickHandler( new ClickHandler(){
			@Override
			public void onClick(ClickEvent event) {
				try {
					HistoryMove move = history.redo(puzzleModel);
					puzzleModel.notifyObservers( move.getPuzzleMove() );
					history.notifyObservers( move );
				} catch (Exception e) {
					e.printStackTrace();
				}
			}} );
		buttonNext.setStylePrimaryName("selectable");

		Image buttonLast = new Image(lastUrl);
		buttonLast.addClickHandler( new ClickHandler(){
			@Override
			public void onClick(ClickEvent event) {
				try {
					HistoryMove move = history.setTime( history.getDuration(), puzzleModel);
					puzzleModel.notifyObservers( move.getPuzzleMove() );
					history.notifyObservers( move );
				} catch (Exception e) {
					e.printStackTrace();
				}
			}} );
		buttonLast.setStylePrimaryName("selectable");
		
		buttonBar.add( buttonFirst );
		buttonBar.add( buttonPrev );
		buttonBar.add( buttonPlay );
		buttonBar.add( buttonPause );
		buttonBar.add( buttonNext );
		buttonBar.add( buttonLast );
		buttonBar.setPixelSize( BUTTON_BAR_WIDTH, BUTTON_BAR_HEIGHT );

		buttonPause.setVisible( false );
		
		playbackSpeedBar = new PlaybackSpeedBar( PLAYBACK_SPEED_BAR_NB_BUTTONS,  moveTimer );
		playbackSpeedBar.setPixelSize( PLAYBACK_SPEED_BAR_WIDTH, PLAYBACK_SPEED_BAR_HEIGHT );
		
		controlPanel.add( buttonBar );
		controlPanel.add( playbackSpeedBar );
		controlPanel.setPixelSize( CONTROL_PANEL_WIDTH, CONTROL_PANEL_HEIGHT );
		
		
		// WORKAROUND FOR GWT LIMITATION
		// Order into which widgets are added to container matters. It would be better to
		// get rid of the addNodeContainer method and create the nodeContainer in EditableHistoryView's
		// constructor, but there is no widget.add method that would let us add the widget at 
		// the right position in the list.
		container.add(barContainer,0,getHeaderHeight());
		barContainer.add(bar,BAR_MARGIN_X,(BAR_CONTAINER_HEIGHT-BAR_HEIGHT)/2);
		addNodeContainer();
		barContainer.add(timeMarker);
		barContainer.add( controlPanel );
		
		initWidget( container );
	
		this.puzzleModel = puzzleModel;
		this.history = history;
		this.moveTimer = moveTimer;

		moveTimer.addObserver( this );
		history.addObserver( this );
		historyViewInfo.addObserver( this );
	}

	/**
	 * Classes that add a header should return the height of this header
	 * 
	 * @return The height of the header, usually 0 when no header was added
	 */
	protected int getHeaderHeight() {
		return 0;
	}

	/**
	 * @return The total height of the History view
	 */
	protected int getTotalHeight() {
		return getHeaderHeight() + BAR_CONTAINER_HEIGHT;
	}
	
	/**
	 * WORKAROUND FOR GWT LIMITATION
	 * See above for more details
	 * Overriden in EditableHistoryView
	 */
	protected void addNodeContainer() {
	}
	
	@Override
	public void screenSizeChanged(Vec2i screenSize) {

		barWidth = screenSize.x - CONTROL_PANEL_WIDTH - 2 * BAR_MARGIN_X;
		if( barWidth < 1 )
			barWidth = 1;
		
		bar.setPixelSize( barWidth, BAR_HEIGHT );
		barContainer.setWidgetPosition( controlPanel, barWidth + 2 * BAR_MARGIN_X, CONTROL_PANEL_Y );
		
		changeTimeMarkerLocation();
	}
	
	@Override
	public void release() {
		history.deleteObserver( this );
		moveTimer.deleteObserver( this );
		historyViewInfo.release();
	}

	@Override
	public void update(Observable observable, Object arg) {

		if( observable == history )
			updateHistory( arg );
		else if( observable == moveTimer )
			updateMoveTimer( arg );
		else if( observable == historyViewInfo )
			updateZoomAndPan();
		else
			assert( false );
	}

	/**
	 * Called whenever the zoom and pan levels are changed
	 */
	protected void updateZoomAndPan() {
		changeTimeMarkerLocation();
	}

	/**
	 * Called whenever history has been updated 
	 * 
	 * @param arg Extra information
	 */
	protected void updateHistory(Object arg) {
	
		changeTimeMarkerLocation();

		if( moveTimer != null && !moveTimer.isRunning() ) {
			moveTimer.set( history.getCurrentTime() );
			moveTimer.notifyObservers();
		}
	}


	/**
	 * Called whenever move timer has been updated 
	 * 
	 * @param arg Extra information
	 */
	private void updateMoveTimer(Object arg) {
		if( moveTimer.isRunning() && buttonPlay.isVisible() ) {
			buttonPlay.setVisible( false );
			buttonPause.setVisible( true );
		}
		else if( !moveTimer.isRunning() && buttonPause.isVisible() ) {
			buttonPlay.setVisible( true );
			buttonPause.setVisible( false );
		}

		long timerTime = moveTimer.peekTime();
		if( timerTime == history.getCurrentTime() ) 
			return;
		try {
			// When stepping outside of history, expand duration
			if( !moveTimer.isRunning() && timerTime > history.getDuration() ) {
				Move move = history.changeDuration( timerTime );
				history.notifyObservers( move );
			}
			HistoryMove move = history.setTime( timerTime, puzzleModel );
			puzzleModel.notifyObservers( move.getPuzzleMove() );
			history.notifyObservers(move);
		} catch (Exception e) {
			e.printStackTrace();
		}
		if( timerTime != history.getCurrentTime() ) {
			moveTimer.stop();
			moveTimer.set( history.getCurrentTime() );
			moveTimer.notifyObservers();
		}
	}
	
	/**
	 * Computes the multiplier to convert from time to X position along horizontal bar
	 * 
	 * @return The conversion multiplier
	 */
	protected float computeTimeToPosXMultiplier() {
		long duration = history.getDuration();
		float ratioMultiplier = 0;
		if( duration > 0 )
			ratioMultiplier = barWidth  * (historyViewInfo.getZoom()+1) / (float)duration;
		return ratioMultiplier;
	}

	/**
	 * Computes the offset to convert from time to X position along horizontal bar
	 * 
	 * @param timeToPosXMultiplier The computed multiplier
	 * @return The conversion offset
	 */
	protected long computeTimeToPosXOffset(float timeToPosXMultiplier) {
		
		// posX = (time - offset) * multiplier
		// pan = 0  ==>  offset = 0
		// pan = 1  ==>  offset = x  s.t.  (duration - offset) * multiplier = barWidth
		//          ==>  offset = duration - (barWidth / multiplier)

		long duration = history.getDuration();
		if( timeToPosXMultiplier == 0 )
			return 0;
		
		return (long)(0.5 + historyViewInfo.getPan() * (duration - (barWidth / timeToPosXMultiplier)));
	}
	
	/**
	 * Computes the multiplier to convert from X position along horizontal bar to time
	 * 
	 * @return The conversion multiplier
	 */
	protected float computePosXToTimeMultiplier() {
		long duration = history.getDuration();
		float ratioMultiplier = 0;
		if( barWidth > 0 )
			ratioMultiplier = (float)duration / (barWidth * (historyViewInfo.getZoom()+1));
		return ratioMultiplier;
	}
	
	/**
	 * Computes the offset to convert from X position along horizontal bar to time
	 * 
	 * @param posXToTimeMultiplier The computed multiplier
	 * @return The conversion offset
	 */
	protected long computePosXToTimeOffset(float posXToTimeMultiplier) {

		// time = offset + posX * multiplier
		// pan = 0  ==>  offset = 0
		// pan = 1  ==>  offset = x  s.t.  offset + barWidth * multiplier = duration
		//          ==>  offset = duration - barWidth * multiplier

		long duration = history.getDuration();

		return (long)(0.5 + historyViewInfo.getPan() * (duration - barWidth * posXToTimeMultiplier));
	}
	
	
	/**
	 * Updates the time marker location 
	 */
	private void changeTimeMarkerLocation() {
		float multiplier = computeTimeToPosXMultiplier();
		changeTimeMarkerLocation( multiplier, computeTimeToPosXOffset(multiplier) ); 
	}
	
	/**
	 * Updates the time marker location (when multiplier is already known)
	 * 
	 * @param timeToPosXMultiplier The multiplier to convert from time to X position
	 * @param timeToPosXOffset The offset to convert from time to X position
	 */
	protected void changeTimeMarkerLocation(float timeToPosXMultiplier, long timeToPosXOffset) {
		long time = history.getCurrentTime();
		int value = (int)((time - timeToPosXOffset) * timeToPosXMultiplier);
		if( value < 0 || value > barWidth )
			timeMarker.setVisible( false );
		else {
			timeMarker.setVisible( true );
			int posX = BAR_MARGIN_X - TIME_MARKER_WIDTH/2 + value;
			int posY = (BAR_CONTAINER_HEIGHT - TIME_MARKER_HEIGHT)/2;
			barContainer.setWidgetPosition( timeMarker, posX, posY );
		}
	}
	

	/**
	 * Converts a position within the time bar to the corresponding time
	 * 
	 * @param posX the position within the time bar
	 */
	private long convertBarPosXtoTime(float posX) {
		float multiplier = computePosXToTimeMultiplier();
		return (long)(posX * multiplier + computePosXToTimeOffset(multiplier));
	}
	
	/**
	 * Set the time to an arbitrary value
	 * 
	 * @param markerPosX The x position within the bar to which to set the time
	 */
	private void setTime( int markerPosX ) {
		if( markerPosX < 0 || markerPosX >= barWidth )
			return;
		try {
			HistoryMove move = history.setTime(convertBarPosXtoTime(markerPosX), puzzleModel);
			puzzleModel.notifyObservers( move.getPuzzleMove() );
			history.notifyObservers( move );
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	

	private class PlaybackSpeedBar extends Composite implements Observer {

		private static final String speedUrl = "timeline/speed.png"; 
		private static final String speedTickUrl = "timeline/speedtick.png"; 

		private final int nbButtons;
		private final MoveTimer moveTimer;
		
		private final Image tickedButtons[]; 
		private final Image untickedButtons[]; 

		private final HorizontalPanel container = new HorizontalPanel();
		
		/**
		 * Creates a bar of buttons that can display the current playback speed
		 * 
		 * @param nbButtons The bumber of buttons on the bar
		 * @param moveTimer The move timer for which to display speed
		 */
		public PlaybackSpeedBar( int nbButtons, final MoveTimer moveTimer ) {
			this.nbButtons = nbButtons;

			this.moveTimer = moveTimer;
			
			tickedButtons = new Image[nbButtons];
			untickedButtons = new Image[nbButtons];
			
			for( int i=0; i<nbButtons; ++i ) {
				tickedButtons[i] = new Image( speedTickUrl );
				tickedButtons[i].setVisible( false );
				untickedButtons[i] = new Image( speedUrl );
				MyClickHandler handler = new MyClickHandler(i);
				tickedButtons[i].addClickHandler( handler );
				untickedButtons[i].addClickHandler( handler );
				
				container.add( tickedButtons[i] );
				container.add( untickedButtons[i] );
			}

			initWidget(container);
			
			moveTimer.addObserver( this );
		}

		@Override
		public void update(Observable observable, Object arg) {
			assert( observable == moveTimer );

			int nbTicks = speedToNbTicks( moveTimer.getSpeed() );
			int i=0;
			for( ; i<nbTicks; ++i ) {
				tickedButtons[i].setVisible( true );
				untickedButtons[i].setVisible( false );
			}
			for( ; i<nbButtons; ++i ) {
				tickedButtons[i].setVisible( false );
				untickedButtons[i].setVisible( true );
			}
		}
		
		private int speedToNbTicks(long speed) {
			int nbTicks = (int)(Math.log(speed) * 1.44269504 + 0.5) + 1;
			if( nbTicks > nbButtons )
				nbTicks = nbButtons;
			return nbTicks;
		}

		private int tickToSpeed(int i) {
			return 1 << i;
		}
				
		@Override
		public void release() {
			moveTimer.deleteObserver( this );
		}
		
		private class MyClickHandler implements ClickHandler {
			private final int tickNumber;
			MyClickHandler( int tickNumber ) {
				this.tickNumber = tickNumber;
			}
			@Override
			public void onClick(ClickEvent event) {
				moveTimer.setSpeed( tickToSpeed(tickNumber) );
				moveTimer.notifyObservers();
			}
		}
	}

	/**
	 * @return The attached history view information
	 */
	public HistoryViewInfo getHistoryViewInfo() {
		return historyViewInfo;
	}
	

	
	public class HistoryViewInfo extends ObservableImpl {
		
		private float zoom;
		private float pan;
		
		
		public void setZoom(float zoom) {
			if( zoom < 0.001 )
				zoom = 0;
			if( this.zoom == zoom )
				return;
			this.zoom = zoom;
			setChanged();
		}
		public float getZoom() {
			return zoom;
		}
		public void setPan(float pan) {
			if( pan < 0.001 )
				pan = 0;
			if( this.pan == pan )
				return;
			else if(pan > 0.99)
				pan = 1;
			this.pan = pan;
			setChanged();
		}
		public float getPan() {
			return pan;
		}
		
		
		
		
	}

	
}
